package com.wx.wxsecondskill.controller;

import static com.wx.redis.constant.RedisConstant.REDIS_EXPIRE;
import static com.wx.wxsecondskill.enums.SecondsKillEnumError.SecondsKillStockError.SECONDS_KILL_ERROR_1;
import static com.wx.wxsecondskill.enums.SecondsKillEnumError.SecondsKillStockError.SECONDS_KILL_STOCK_ERROR_0;
import static com.wx.wxsecondskill.init.GoodsStockCountCommandLineRunner.GOODSSTOCKREFIX;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;

import com.wx.redis.RedisUtils;
import com.wx.wxsecondskill.po.OrderInfo;
import com.wx.wxsecondskill.po.SecondsKillOrder;
import com.wx.wxsecondskill.po.User;
import com.wx.wxsecondskill.service.GoodsService;
import com.wx.wxsecondskill.service.OrderService;
import com.wx.wxsecondskill.service.SecondsKillService;
import com.wx.wxsecondskill.service.UserService;
import com.wx.wxsecondskill.vo.GoodsVo;

/**
 * @ClassName SecondsKillController
 * @Author wx
 * @Description 秒杀操作Controller
 * @Date 2018-11-15-22:15
 */
@Controller
@RequestMapping("/secondsKill")
public class SecondsKillController {
	/** 用户秒杀商品记录key前缀 */
	public final static String USERSECONDSKILREFIX = "user_and_good_";

	/** 用户缓存key前缀 */
	private final static String USERPREFIX = "user_";

	/** 订单页面缓存key前缀 */
	private final static String ORDERPREFIX = "order_";

	/** 秒杀提示错误缓存key前缀 */
	private final static String ERRPREFIX = "err_";

	@Autowired
	private OrderService orderService;

	@Autowired
	private GoodsService goodsService;

	@Autowired
	private SecondsKillService secondsKillService;

	@Autowired
	private UserService userService;

	@Autowired
	private ThymeleafViewResolver thymeleafViewResolver;

	/**
	 * @methodName: miaosha
	 * @author: wx
	 * @description: 秒杀原方法 200用户 3000*2/s并发 QPS=627  第二次QPS=1024
	 * @param goodsId
	 * @param userId
	 * @date: 2018/11/17
	 * @return: org.springframework.web.servlet.ModelAndView
	 */
	@PostMapping("/do_miaosha")
	public ModelAndView miaosha(@RequestParam("goodsId") Integer goodsId, @RequestParam("userId") String userId) {
		ModelAndView modelAndView = new ModelAndView("order_detail");
		User user = userService.getById(userId);
		if (user == null) {
			modelAndView.addObject("errmsg", "用户不存在");
			modelAndView.setViewName("miaosha_fail");
			return modelAndView;
		}
		modelAndView.addObject("user", user);

		// 判断库存
		GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
		int stock = goods.getStockCount();
		if (stock <= 0) {
			modelAndView.addObject("errmsg", SECONDS_KILL_STOCK_ERROR_0.getMessageError());
			modelAndView.setViewName("miaosha_fail");
			return modelAndView;
		}
		// 判断是否已经秒杀到了
		SecondsKillOrder secondsKillOrder = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
		if (secondsKillOrder != null) {
			modelAndView.addObject("errmsg", SECONDS_KILL_ERROR_1.getMessageError());
			modelAndView.setViewName("miaosha_fail");
			return modelAndView;
		}
		// 减库存 下订单 写入秒杀订单
		OrderInfo orderInfo = secondsKillService.miaosha(user, goods);
		modelAndView.addObject("orderInfo", orderInfo);
		modelAndView.addObject("goods", goods);
		return modelAndView;
	}

	/**
	 * @methodName: miaoshaV2
	 * @author: wx
	 * @description: 秒杀优化方法 200用户 3000*2/s并发 QPS=910  第二次QPS=1518
	 *               1.利用数据库的一致性，防止库存超卖，在减库存的时候带上and stock_count > 0）
	 *               2.利用数据库的唯一性，使用复合唯一索引,防止高并发下同一个用户秒杀同一个商品多次
	 *               3.用户直接从缓存获取
	 *               4.页面缓存
	 *               5.缓存预减库存
	 *               6.用户秒杀商品记录缓存
	 *               7.商品信息缓存
	 * @param goodsId
	 * @param userId
	 * @date: 2018/11/17
	 * @return: org.springframework.web.servlet.ModelAndView
	 */
	@ResponseBody
	@PostMapping(value = "/do_miaoshaV2", produces = "text/html;charset=UTF-8")
	public String miaoshaV2(@RequestParam("goodsId") Integer goodsId, @RequestParam("userId") String userId,
		HttpServletRequest request, HttpServletResponse response) {
		/**
		 * 此处可以做个简单的限流，具体思路可以利用redis计数来做，比如我5秒只允许500访问，
		 * 设置一个计数值为1，过期时间为5秒，每次访问+1，当大于100的时候就返回提示不做任何操作
		 * 5秒后key过期被删除，再次访问这个key发现不存在，继续设置一个计数值为1，过期时间为5秒，
		 * 这只是一个简单的做法，并不严谨，做不到真正的5秒/500次 会有所偏差。
		 * 限流算法有三种令牌桶、漏桶、计数器。
		 */
		String errHtml;
		WebContext ctx = new WebContext(request, response, request.getServletContext(), request.getLocale());
		Map<String, Object> dataMap = new HashMap<>();
		User user = (User) RedisUtils.redisClient.get(USERPREFIX + userId);
		if (user == null) {
			user = userService.getById(userId);
			if (user == null) {
				// 防止缓存穿透
				RedisUtils.redisClient.set(USERPREFIX + userId, new User("err500"), 60);
				dataMap.put("errmsg", "用户不存在");
				ctx.setVariables(dataMap);
				errHtml = thymeleafViewResolver.getTemplateEngine().process("miaosha_fail", ctx);
				return errHtml;
			}
			RedisUtils.redisClient.set(USERPREFIX + userId, user, REDIS_EXPIRE);
		}

		// 防止缓存穿透
		if (Objects.equals(user.getId(), "err500")) {
			return null;
		}

		// redis缓存预减库存 -1
		long stockCount = RedisUtils.redisClient.decr(GOODSSTOCKREFIX + goodsId, 1);
		if (stockCount < 0) {
			errHtml = (String) RedisUtils.redisClient.get(ERRPREFIX);
			if (StringUtils.isBlank(errHtml)) {
				return errHtml(ERRPREFIX, SECONDS_KILL_STOCK_ERROR_0.getMessageError(), dataMap, ctx);
			}
			return errHtml;
		}
		// 判断是否已经秒杀到了
		SecondsKillOrder secondsKillOrder = orderService.userIsSecondsKillGoods(user.getId(), goodsId);
		if (secondsKillOrder != null) {
			errHtml = (String) RedisUtils.redisClient.get(ERRPREFIX + userId);
			if (StringUtils.isBlank(errHtml)) {
				return errHtml(ERRPREFIX + userId, SECONDS_KILL_ERROR_1.getMessageError(), dataMap, ctx);
			}
			return errHtml;
		}
		/**   db减库存与创建订单  此处可以做队列异步减缓存与创建订单优化，思路如下
		 * 	  1.此次为生成端消息入队列，把商品与用户信息传递过去
		 * 	  2.直接返回一个标识0  （假设 0-排队中 1-秒杀失败 2-秒杀成功）
		 * 	  3.消费端消费队列消息进行db减库存与创建订单并把用户秒杀记录设置进缓存的操作
		 * 	  4.假设db减库存失败，如果没有抛出异常说明库存为0，这时候往reids里面设置一个该商品库存为0的标识key
		 * 	  5.如果(db减库存与创建订单并把用户秒杀记录设置进缓存的操作)抛出异常，往redis里面设置一个处理(db减库存与创建订单并把用户秒杀记录设置进缓存的操作)的服务出现异常的key
		 * 	  6.客户端设置一个轮询，去访问用户秒杀商品记录接口，发现有记录则直接返回2-秒杀成功，否则去验证商品库存为0的标识key是否存在，存在的话返回1秒杀失败库存为0
		 * 	  不存在就去验证处理订单服务异常标识是否存在，存在的话返回1秒杀失败服务异常请稍后再试,不存在就直接返回0-排队中，客户端接着轮询
		 *
		 * 	  ps:当然这是比较理想的情况，因为这里涉及到了分布式事务，在此不进行展开
		 * */
		GoodsVo goods  = (GoodsVo)RedisUtils.redisClient.get("goods_id_" + goodsId);
		if (goods == null){
			goods =  goodsService.getGoodsVoByGoodsId(goodsId);
			if (goods != null){
				RedisUtils.redisClient.set("goods_id_" + goodsId, goods, 3000);
			}else {
				return null;
			}
		}
/*		GoodsVo  goods =  goodsService.getGoodsVoByGoodsId(goodsId);*/
		OrderInfo orderInfo = secondsKillService.miaoshV2(user, goods);
		String orderHtml = (String) RedisUtils.redisClient.get(ORDERPREFIX + userId);
		if (StringUtils.isBlank(orderHtml)) {
			dataMap.put("user", user);
			dataMap.put("orderInfo", orderInfo);
			dataMap.put("goods", goods);
			ctx.setVariables(dataMap);
			orderHtml = thymeleafViewResolver.getTemplateEngine().process("order_detail", ctx);
			return orderHtml;
		}
		return orderHtml;
	}

	/**
	 * @methodName: errHtml
	 * @author: wx
	 * @description: 解析并缓存秒杀错误页面信息
	 * @param keyPrefix 缓存key前缀
	 * @param messageError 错误信息
	 * @param dataMap 数据容器
	 * @param ctx web上下文
	 * @date: 2018/11/18
	 * @return: java.lang.String
	 */
	private String errHtml(String keyPrefix, String messageError, Map<String, Object> dataMap, WebContext ctx) {
		dataMap.put("errmsg", messageError);
		ctx.setVariables(dataMap);
		String errHtml = thymeleafViewResolver.getTemplateEngine().process("miaosha_fail", ctx);
		RedisUtils.redisClient.set(keyPrefix, errHtml, 60);
		return errHtml;
	}
}
